﻿using System;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Description;
using Aluetjen.EMailService.Contract;
using Aluetjen.EmailService.Domain.Infrastructure;
using Aluetjen.EmailService.Domain.Models.Endpoints;
using log4net;

namespace Aluetjen.EmailService.Agent
{
    public class HostManager : IHostManager
    {
        private readonly ILog _log = LogManager.GetLogger(typeof (HostManager));

        private readonly IBindingFactory _bindingFactory;
        private readonly IRepository<Endpoint> _endpointRepository;
        private readonly HostContainer _hostContainer;

        public HostManager(IRepository<Endpoint> endpointRepository, IBindingFactory bindingFactory, HostContainer hostContainer)
        {
            _endpointRepository = endpointRepository;

            _hostContainer = hostContainer;
            _bindingFactory = bindingFactory;
            _hostContainer = hostContainer;
        }

        public void OpenMissingEndpoints()
        {
            lock (_hostContainer.Sync)
            {
                foreach (var endpointMetadata in _endpointRepository.All().Where(e => e.Enabled))
                {
                    if (ValidateExistingHost(endpointMetadata))
                        continue;

                    if (!TryOpenHost(endpointMetadata))
                    {
                        _log.WarnFormat(
                            "Failed to open endpoint {0} of type {1} at {2}. Will try again next polling interval.",
                            endpointMetadata.Id, endpointMetadata.Binding, endpointMetadata.Address);
                    }
                }
            }
        }

        public void CloseAllEndpoints()
        {
            _log.Info("Closing all hosts for all endpoints...");

            lock (_hostContainer.Sync)
            {
                foreach(var host in _hostContainer.OpenedHosts)
                {
                    CloseHostOfEndpoint(host.Value, host.Key);
                }
            }
        }

        private bool TryOpenHost(Endpoint endpointMetadata)
        {
            ServiceEndpoint endpoint;

            try
            {
                endpoint = ConfigureServiceEndpoint(endpointMetadata);
            }
            catch (Exception exception)
            {
                _log.ErrorFormat("Failed to configure endpoint {0}: {1}", endpointMetadata.Id, exception);
                return false;
            }

            var host = new ServiceHost(typeof (EmailService), new Uri(endpointMetadata.Address));
            host.AddServiceEndpoint(endpoint);

            if (host.BaseAddresses.Where(b => string.Compare(b.Scheme, "http", true) == 0).Count() > 0)
            {
                var serviceMetadataBehavior = new ServiceMetadataBehavior {HttpGetEnabled = true};

                host.Description.Behaviors.Add(serviceMetadataBehavior);
            }

            var throttle = new ServiceThrottlingBehavior {MaxConcurrentCalls = 50};
            host.Description.Behaviors.Add(throttle);

            _log.InfoFormat("Opening endpoint for {0} of binding {1}...", endpointMetadata.Id, endpointMetadata.Binding);

            try
            {
                host.Open();
            }
            catch (CommunicationException exception)
            {
                _log.ErrorFormat("Failed to open service host for endpoint {0}: {1}", endpointMetadata.Id, exception);
                return false;
            }

            _hostContainer.LoadedEndpoints.Add(endpointMetadata);
            _hostContainer.OpenedHosts[endpointMetadata.Id] = host;

            return true;
        }

        private ServiceEndpoint ConfigureServiceEndpoint(Endpoint endpointMetadata)
        {
            var binding = _bindingFactory.Create(endpointMetadata.Binding, endpointMetadata.Binding + "Config");

            var endpoint = new ServiceEndpoint(
                ContractDescription.GetContract(typeof (IEmailService), typeof(EmailService)),
                binding, new EndpointAddress(endpointMetadata.Address));

            return endpoint;
        }

        private bool ValidateExistingHost(Endpoint endpoint)
        {
            bool hasValidHost = false;

            ServiceHost openedHost;
            if (_hostContainer.OpenedHosts.TryGetValue(endpoint.Id, out openedHost))
            {
                bool isOperating = openedHost.State == CommunicationState.Opened ||
                                   openedHost.State == CommunicationState.Opening;

                bool isUptodate = _hostContainer.LoadedEndpoints.Where(e => e.Id == endpoint.Id && e.Version == endpoint.Version).Count() == 1;

                if (isOperating && isUptodate)
                {
                    _log.DebugFormat("Host for endpoint {0} already open or opening.", endpoint.Id);
                    hasValidHost = true;
                }
                else
                {
                    CloseHostOfEndpoint(openedHost, endpoint.Id);

                    // Remove endpoint and host from state
                    _hostContainer.OpenedHosts.Remove(endpoint.Id);
                    _hostContainer.LoadedEndpoints.RemoveAll(e => e.Id == endpoint.Id);
                }
            }

            return hasValidHost;
        }

        private void CloseHostOfEndpoint(ServiceHost host, int endpointId)
        {
            try
            {
                host.Close();
            }
            catch (Exception exception)
            {
                _log.ErrorFormat("Failed to close host for endpoint {0}: {1}", endpointId, exception);

                try
                {
                    host.Abort();
                }
                catch (Exception abortException)
                {
                    _log.ErrorFormat("Failed to abort host for endpoint {0}: {1}", endpointId, abortException);
                }
            }
        }
    }
}
